昨天用ApplicationMailer
實作了訂單成立後寄送email,不知道大家有沒有發現寄 email 這個工作花費的時間特別久,我的體感時間大概要三秒,如果讓客戶成立訂單後等三秒才能看到訂單完成頁,他應該下次就不想買了(´-ι_-`)
不只是寄送email,產生excel/csv檔,或是解析excel/csv檔...等,都是要耗費不少時間的工作,依照 Rails server 原來的設定,等一個工作結束後才能再做下一個,但是在生活節奏如此快的現代,這樣應該生意也不用做了。所以今天的主題背景執行工作
就是必要的功能。當我們把一個工作移到背景去執行,server就不用等待這個工作執行完成才能做下一動 (又不是當兵),也就達到異步
的效果。
今天的目標是把昨天的email寄送移到背景執行
使用 Rails 的 ActiveJob 產生器g job
,工作命名為order_create_email
在Command line 執行:
bin/rails g job order_create_email
產生了兩個檔案:
invoke test_unit
create test/jobs/order_create_email_job_test.rb
create app/jobs/order_create_email_job.rb
直接來看app/jobs/order_create_email_job.rb
:
class OrderCreateEmailJob < ApplicationJob
queue_as :default
def perform(*args)
# Do something later
end
end
queue_as
後面帶的參數是決定這個工作會不會很急,可以用這三個參數:
:default
:low_priority
:urgent
這個job到底要做什麼內容,是放在perform(*args)
這個block
內。我修改block內容為訂單成立後寄信:
class OrderCreateEmailJob < ApplicationJob
queue_as :default
def perform(order_id)
products = Order.find(order_id).products
AdminMailer.notify_customer(products).deliver_now
end
end
同時修改原本要立刻寄信的Order Model after_create
class Order < ApplicationRecord
after_create { OrderCreateEmailJob.perform_later(self.id) }
has_many :products
end
使用perform_later
這個class_method
,並把參數order_id
帶入。到此就算設定完成了,可以用背景工作寄信囉。
另外,如果想要指定什麼時候做的話,可以這樣寫:
OrderCreateEmailJob.set(wait: 2.seconds).perform_later(self.id)
OrderCreateEmailJob.set(wait_until: Date.tomorrow.noon).perform_later(self.id)
跟昨天一樣在console
建立一筆訂單:
Order.create!(name: '#3', products: Product.all)
可以看到系統有job 的log:
Enqueued OrderCreateEmailJob (Job ID: c6131b30-6dc9-4609-ac5c-da70125adcad) to Async(default) with arguments: 14
Performing OrderCreateEmailJob (Job ID: c6131b30-6dc9-4609-ac5c-da70125adcad) from Async(default) with arguments: 14
...
...中間省略...
...
Performed OrderCreateEmailJob (Job ID: c6131b30-6dc9-4609-ac5c-da70125adcad) from Async(default) in 7360.53ms
Order.create!
大致上可以看到,Rails 先Enqueue
(排進執行序)一個job,再Performing
(開始執行),最後Performed
(執行完成)。
每個job
都有屬於自己的job-id
,這個 job 的id是c6131b30-6dc9-4609-ac5c-da70125adcad
。
這樣我們就不用等到信寄出後才能執行下個請求,能夠讓工作分配更有效率。
「排隊」(Queue)預設是會把排程放在記憶體裡,但如果萬一伺服器當機或重開機,這個排隊的資料就不見了。在實務上常會另外設置可以排隊的地方,常見的有 Sidekiq
跟 Delayed Job
,這裡介紹Sidekiq
做排程。
因為Sidekiq
使用Redis
這個database store 儲存任務,而Redis
透過key-value來儲存資料,Redis
的多process執行,能夠同時執行多個任務。
Redis
brew install redis
在Gemfile新增
gem 'redis', '~> 4.0'
gem 'redis-rails'
如果要使用Redis來cache 的資料,還要再安裝gem 'redis-namespace'
跟gem 'redis-rack-cache'
。
Sidekiq
在Gemfile新增
gem 'sidekiq'
Sidekiq
我們需要啟動另外的sidekiq process來執行這些非同步的任務:
bundle exec sidekiq
預設的ActiveJob Adapter是:inline,也就是沒有非同步。要改一下 ActiveJob 內建存放工作的地方,請編輯 app/configs/application.rb
:
module Todolist
class Application < Rails::Application
config.active_job.queue_adapter = :sidekiq
end
end
(Todolist 是這個Rails 專案的名字)
好了,前置作業完成。
讓我們再來執行一次訂單建立,並觸發after_create
一樣在console
建立一筆訂單:
Order.create!(name: '#4', products: Product.all)
觀察console的log可以發現,在使用sidekiq之前信件內容/寄件人/收件人會跟SQL語法印在一起,使用Sidekiq後信件log跑到剛才執行bundle exec sidekiq
的頁面:
可以看到最後兩行紀錄著剛才class=OrderCreateEmailJob
還有jid=da9f2157ee5adb5faf45530a
。
這樣異步工作就算完成了。
Sidekiq
本身的功能非常強大,可以透過Sidekiq::Status
來確認工作的狀態是queued,working,failed,completed
那一個,甚至可以知道現在進度是幾%了。如果再搭配sidekiq-scheduler
,還可以做到預設工作時間及循環執行的功能,像我想要每個月的第一天計算上個月賺了多少錢,再把帳務明細寄出,這些都可以辦到。
以上是用 Rails 原生的ActiveJob來做背景工作,既然我們都用了Sidekiq,也可以改用Sidekiq內建的worker
。
首先在app
底下新增workers
資料夾,再新增 order_create_email_worker.rb
這個檔案,把剛才放在app/jobs/order_create_email_job.rb
的內容搬進來:
class OrderCreateEmailWorker
include Sidekiq::Worker
sidekiq_options retry: true
def perform(order_id)
products = Order.find(order_id).products
AdminMailer.notify_customer(products).deliver_now
end
end
第二行include Sidekiq::Worker
把worker
的方法導入。
第三行retry: true
,當worker失敗時,自動重新執行。
最後把原本呼叫ActiveJob的地方改成呼叫Sidekiq::Worker。app/models/order.rb
換成worker
就可以了
class Order < ApplicationRecord
after_create { OrderCreateEmailWorker.perform_async(id) }
has_many :products
end
用這張圖做結尾
Courtesy of https://www.youtube.com/watch?v=GBEDvF1_8B8
原本由Controller來分配工作,等一個工作執行完再跑下一個。現在透過Redis
來Queue(排程)讓工作分頭進行,我們只要提示客戶工作已經排程處理,就可以放心讓 Rails 應用程式繼續執行其他任務。